﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using LINQPad.Extensibility.DataContext;
using LINQPad.OpenAccess.Helpers;
using LINQPad.OpenAccess.Infrastructure;
using LINQPad.OpenAccess.Services;
using LINQPad.OpenAccess.Settings;
using Ninject;
using Telerik.OpenAccess;

namespace LINQPad.OpenAccess
{
    /// <summary>
    ///   <see cref="StaticDataContextDriver" /> for Telerik's OpenAccess ORM.
    /// </summary>
    public partial class OpenAccessStaticDataContextDriver : StaticDataContextDriver
    {
        #region Private

        #region - Vars

        private readonly string _author, _name;
        private IDataContextServices _dataContextServices;
        private bool _isInitialized;

        #endregion

        #region - Functions

        /// <summary>
        /// Gets the author from the driver's assembly.
        /// </summary>
        /// <returns>
        /// The author.
        /// </returns>
        /// <exception cref="System.InvalidOperationException">Failed to retrieve the author information from the driver's assembly !</exception>
        private string GetAuthorFromAssembly( )
        {
            var attr = GetAttributeFromAssembly<AssemblyCompanyAttribute>( );
            if ( attr == null )
                throw new InvalidOperationException( "Failed to retrieve the author information from the driver's assembly !" );
            return attr.Company;
        }
        /// <summary>
        /// Gets the context type from the specified settings.
        /// </summary>
        /// <param name="driverSettings">The driver settings.</param>
        /// <returns>
        /// The context type.
        /// </returns>
        /// <exception cref="System.InvalidOperationException">If the type of the data context is not properly configured.</exception>
        private Type GetContextTypeFromSettings( DriverSettings driverSettings )
        {
            Guard.ArgumentNotNull( driverSettings, "driverSettings" );
            if ( String.IsNullOrWhiteSpace( driverSettings.AssemblyQualifiedContextTypeName ) )
                throw new InvalidOperationException( "The type of the data context is not properly configured !" );

            return Type.GetType( driverSettings.AssemblyQualifiedContextTypeName, throwOnError: true );
        }
        /// <summary>
        /// Gets the name from assembly.
        /// </summary>
        /// <returns>
        /// The name.
        /// </returns>
        /// <exception cref="System.InvalidOperationException">Failed to retrieve the product information from the driver's assembly !</exception>
        private string GetNameFromAssembly( )
        {
            var attr = GetAttributeFromAssembly<AssemblyProductAttribute>( );
            if ( attr == null )
                throw new InvalidOperationException( "Failed to retrieve the product information from the driver's assembly !" );
            var name = attr.Product;
            var suffixToRemove = "- LINQPad driver";
            if ( name.EndsWith( suffixToRemove ) )
                name = name.Substring( 0, name.Length - suffixToRemove.Length )
                           .Trim( );
            return name;
        }

        #endregion

        #endregion

        #region Protected

        #region - Functions

        /// <summary>
        /// Ensures that the specified service is availabled.
        /// </summary>
        /// <typeparam name="TService">The type of the service.</typeparam>
        /// <param name="serviceSelector">The service selector.</param>
        /// <returns>The service.</returns>
        protected virtual TService EnsureService<TService>( Func<IDataContextServices, TService> serviceSelector )
        {
            Guard.ArgumentNotNull( serviceSelector, "serviceSelector" );

            var service = serviceSelector( this._dataContextServices );
            if ( service == null )
                throw new InvalidOperationException( String.Format( "The requested data context service is not available ! (ServiceType: {0})", typeof( TService ).FullName ) );
            return service;
        }
        /// <summary>
        /// Ensures that the connection info provides driver settings and returns them.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <returns>
        /// The driver settings.
        /// </returns>
        /// <exception cref="System.InvalidOperationException">If the <paramref name="connectionInfo" /> doesn't provide driver settings.</exception>
        private DriverSettings EnsureSettings( IConnectionInfo connectionInfo )
        {
            var settings = this.EnsureService( f => f.SettingsRepository )
                               .GetSettings( connectionInfo );
            if ( settings == null )
                throw new InvalidOperationException( "The connection info doesn't provide valid driver settings !" );
            return settings;
        }
        /// <summary>
        /// Throws an exception if the driver is not yet initialized.
        /// </summary>
        /// <exception cref="System.InvalidOperationException">If the driver is not yet initialized.</exception>
        protected virtual void ThrowIfNotInitialized( )
        {
            if ( !this._isInitialized )
                throw new InvalidOperationException( String.Format( "The data context driver is not (yet?) initialized ! (DriverType: {0})", this.GetType( ).FullName ) );
        }

        #endregion

        #region - Properties

        /// <summary>
        /// Gets the services.
        /// </summary>
        /// <value>
        /// The services.
        /// </value>
        protected internal IDataContextServices Services
        {
            get
            {
                return this._dataContextServices;
            }
        }

        #endregion

        #endregion

        #region Public

        #region - Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="OpenAccessStaticDataContextDriver" /> class.
        /// </summary>
        public OpenAccessStaticDataContextDriver( )
        {
            this._author = this.GetAuthorFromAssembly( );
            this._name = this.GetNameFromAssembly( );

            var serviceLocator = ServiceLocator.Current;
            // FIXME: ninject is not reliable in this case.
            //        e.g. on unit tests, sometimes ninject isn't ready yet
            //        => the Inject() call is silently ignored in this case
            // serviceLocator.Inject( this );

            // The solution below still suffers from the same problem,
            // but instead of a silent death, this time the get call returns null
            // => the InitializeDataContextDriver throws at least an ArgumentNullException

            var dataContextServices = serviceLocator.Get<IDataContextServices>( );

            this.InitializeDataContextDriver( dataContextServices );
        }
        /// <summary>
        /// Initializes a new instance of the <see cref="OpenAccessStaticDataContextDriver" /> class.
        /// </summary>
        /// <param name="dataContextServices">The data context services.</param>
        public OpenAccessStaticDataContextDriver( IDataContextServices dataContextServices )
        {
            this._author = this.GetAuthorFromAssembly( );
            this._name = this.GetNameFromAssembly( );
            this.InitializeDataContextDriver( dataContextServices );
        }

        #endregion

        #region - Functions

        /// <summary>
        /// Initializes the data context driver.
        /// </summary>
        /// <param name="dataContextServices">The data context services.</param>
        /// <exception cref="System.InvalidOperationException">If the driver is already initialize</exception>
        [Inject]
        public void InitializeDataContextDriver( IDataContextServices dataContextServices )
        {
            Guard.ArgumentNotNull( dataContextServices, "dataContextServices" );

            if ( this._isInitialized )
                throw new InvalidOperationException( String.Format( "The data context driver is already initialized ! (DriverType: {0})", this.GetType( ).FullName ) );

            this._isInitialized = true;
            this._dataContextServices = dataContextServices;
        }

        #endregion

        #region - Properties

        /// <summary>
        /// Gets a value indicating whether the driver is initialized.
        /// </summary>
        /// <value>
        /// <c>true</c> if the driver is initialized; otherwise, <c>false</c>.
        /// </value>
        public bool IsInitialized
        {
            get
            {
                return this._isInitialized;
            }
        }

        #endregion

        #endregion

        #region StaticDataContextDriver Members

        #region - Functions
        /// <summary>
        /// Gets the assemblies to add.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <returns></returns>
        public override IEnumerable<string> GetAssembliesToAdd( IConnectionInfo connectionInfo )
        {
            yield return "Telerik.OpenAccess.dll";
            yield return "Telerik.OpenAccess.35.Extensions.dll";
        }
        /// <summary>
        /// Gets the connection description.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <returns>
        /// The description.
        /// </returns>
        public override string GetConnectionDescription( IConnectionInfo connectionInfo )
        {
            if ( connectionInfo == null )
                return null;

            var provider = this.Services.ConnectionDescriptionProvider;
            if ( provider != null )
            {
                var repos = this.Services.SettingsRepository;
                var driverSettings = repos == null ? null : repos.GetSettings( connectionInfo );
                return provider.GetDescription( connectionInfo, driverSettings );
            }
            return null;
        }
        /// <summary>
        /// Gets the context constructor arguments.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <returns>
        /// The constructor arguments.
        /// </returns>
        public override object[] GetContextConstructorArguments( IConnectionInfo connectionInfo )
        {
            var settings = this.EnsureSettings( connectionInfo );
            var contextType = this.GetContextTypeFromSettings( settings );

            var initializationData = this.EnsureService( f => f.InitializationService )
                                         .GetContextInitializationData( contextType, connectionInfo, settings );
            if ( initializationData == null )
                return new object[0];

            return new object[] 
                    {
                        initializationData.ConnectionString
                        , initializationData.BackendConfiguration
                        , initializationData.MetadataSource
                    };
        }
        /// <summary>
        /// Gets the context constructor parameters.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <returns></returns>
        public override ParameterDescriptor[] GetContextConstructorParameters( IConnectionInfo connectionInfo )
        {
            ParameterDescriptor[] parameters;
            if ( !this.EnsureService( f => f.InitializationService )
                       .TryGetContextConstructorSignature( connectionInfo, this.EnsureSettings( connectionInfo ).Initialization, out parameters ) )
                parameters = new ParameterDescriptor[0]; // fallback to parameterless ctor.
            return parameters;
        }
        /// <summary>
        /// Gets the custom display member provider.
        /// </summary>
        /// <param name="objectToWrite">The object to write.</param>
        /// <returns>
        /// The custom member provider or <c>null</c>.
        /// </returns>
        public override ICustomMemberProvider GetCustomDisplayMemberProvider( object objectToWrite )
        {
            var factory = this.Services.CustomMemberProviderFactory;
            if ( factory != null )
                return factory.GetCustomDisplayMemberProvider( objectToWrite );
            return null;
        }
        /// <summary>
        /// Gets the namespaces to add.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <returns></returns>
        public override IEnumerable<string> GetNamespacesToAdd( IConnectionInfo connectionInfo )
        {
            yield return "Telerik.OpenAccess";
            yield return "Telerik.OpenAccess.Diagnostics";
            yield return "Telerik.OpenAccess.FetchOptimization";
        }
        /// <summary>
        /// Gets the namespaces to remove.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <returns></returns>
        public override IEnumerable<string> GetNamespacesToRemove( IConnectionInfo connectionInfo )
        {
            return this.GetNamespacesToAdd( connectionInfo );
        }
        /// <summary>
        /// Gets the schema.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="customType">Type of the context.</param>
        /// <returns>
        /// The schema.
        /// </returns>
        public override List<ExplorerItem> GetSchema( IConnectionInfo connectionInfo, Type customType )
        {
            Guard.ArgumentNotNull( connectionInfo, "connectionInfo" );
            Guard.ArgumentNotNull( customType, "customType" );



            var settings = this.EnsureSettings( connectionInfo );
            var contextMetadata = this.EnsureService( f => f.InitializationService )
                                      .GetContextMetadata( customType, settings.MetadataSource );

            return this.EnsureService( f => f.SchemaProvider )
                       .GetSchema( customType, contextMetadata );
        }
        /// <summary>
        /// Initializes the context.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="context">The context.</param>
        /// <param name="executionManager">The execution manager.</param>
        public override void InitializeContext( IConnectionInfo connectionInfo, object context, QueryExecutionManager executionManager )
        {
            base.InitializeContext( connectionInfo, context, executionManager );
            var typedContext = context as OpenAccessContextBase;
            if ( typedContext != null )
                typedContext.Log = executionManager.SqlTranslationWriter; // enable context/sql tracing
        }
        /// <summary>
        /// Shows the connection dialog.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="isNewConnection">if set to <c>true</c> if it's a new connection.</param>
        /// <returns>
        ///   <c>true</c> on success; otherwise, <c>false</c>.
        /// </returns>
        public override bool ShowConnectionDialog( IConnectionInfo connectionInfo, bool isNewConnection )
        {
            DriverSettings driverSettings = null;

            var settingsRepository = this.EnsureService( f => f.SettingsRepository );
            if ( !isNewConnection )
                driverSettings = settingsRepository.GetSettings( connectionInfo );
            if ( driverSettings == null )
                driverSettings = settingsRepository.GetDefaultSettings( connectionInfo );

            var saveSettings = this.EnsureService( e => e.UserInteractionService )
                                   .EditConfiguration( connectionInfo, driverSettings );

            if ( saveSettings )
                settingsRepository.SaveSettings( connectionInfo, driverSettings );

            return saveSettings;
        }
        /// <summary>
        /// Tears down context.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="context">The context.</param>
        /// <param name="executionManager">The execution manager.</param>
        /// <param name="constructorArguments">The constructor arguments.</param>
        public override void TearDownContext( IConnectionInfo connectionInfo, object context, QueryExecutionManager executionManager, object[] constructorArguments )
        {
            var typedContext = context as OpenAccessContextBase;
            if ( typedContext != null )
                typedContext.Log = null;

            base.TearDownContext( connectionInfo, context, executionManager, constructorArguments );
        }
        #endregion

        #region - Properties

        /// <summary>
        /// Gets the author.
        /// </summary>
        /// <value>
        /// The author.
        /// </value>
        public override string Author
        {
            get
            {
                return this._author;
            }
        }
        /// <summary>
        /// Gets the name.
        /// </summary>
        /// <value>
        /// The name.
        /// </value>
        public override string Name
        {
            get
            {
                return this._name;
            }
        }

        #endregion

        #endregion
    }
}